Skip to content

refactor: introduce typed SceneAccessor and TimerAccessor#85

Merged
Faerkeren merged 1 commit into
mainfrom
feature/typed-domain-accessors
May 27, 2026
Merged

refactor: introduce typed SceneAccessor and TimerAccessor#85
Faerkeren merged 1 commit into
mainfrom
feature/typed-domain-accessors

Conversation

@Faerkeren
Copy link
Copy Markdown
Contributor

Closes #76

Issue validity

Valid and actionable. The issue correctly identifies that:

  • DomainSpec.operations (dict[str, Callable[..., Any]]) and dynamic setattr binding make collection-level domain operations invisible to type checkers.
  • Both scene.py and timer.py worked around this by accessing the private accessor._factory with # type: ignore[assignment], bypassing encapsulation.
  • The project ships py.typed and runs mypy in strict mode, so this is a genuine gap.

Fix

core/plugins.py

  • Added accessor_cls: type[DomainAccessor[Any]] | None = None field to DomainSpec. Domains with collection-level operations declare their typed accessor subclass here instead of populating operations.
  • Added DomainAccessor.factory as a public property returning the EntityFactoryProtocol. Subclasses use this to reach factory.services and factory.state without touching _factory.

api.py

  • HAClient.__init__ now instantiates spec.accessor_cls when set, falling back to the base DomainAccessor for domains that don't need collection-level operations.

domains/scene.py

  • Replaced free functions _create / _apply + operations={...} with a SceneAccessor(DomainAccessor[Scene]) subclass exposing typed async def create(...) -> Scene and async def apply(...) -> None methods.
  • DomainSpec now uses accessor_cls=SceneAccessor.

domains/timer.py

  • Replaced free function _create + operations={...} with a TimerAccessor(DomainAccessor[Timer]) subclass exposing a typed async def create(...) -> Timer method.
  • DomainSpec now uses accessor_cls=TimerAccessor.
  • All # type: ignore[assignment] comments removed.

Tests added (tests/test_plugins.py)

Test What it verifies
test_domain_accessor_factory_property DomainAccessor.factory returns an EntityFactory instance
test_scene_accessor_is_typed_subclass ha.scene is a SceneAccessor with callable create and apply
test_timer_accessor_is_typed_subclass ha.timer is a TimerAccessor with a callable create
test_accessor_cls_in_spec_is_used Custom accessor_cls on a spec is correctly instantiated
test_accessor_cls_none_falls_back_to_base Specs without accessor_cls get the base DomainAccessor

Validation

  • pytest tests/ --cov=haclient --cov-fail-under=95: 316 passed, coverage 97.11%
  • ruff check src tests: All checks passed
  • ruff format --check src tests: 57 files already formatted
  • mypy src: Success: no issues found in 38 source files

- Add `accessor_cls` field to `DomainSpec` so domains can declare a
  typed `DomainAccessor` subclass instead of relying on dynamic
  `setattr`-based operation binding.
- Expose `DomainAccessor.factory` as a public property so subclasses
  can access services/state without reaching into `_factory` private
  attribute.
- `HAClient` now instantiates `spec.accessor_cls` when set, falling
  back to the base `DomainAccessor` for domains without one.
- Replace scene domain's dynamic `_create`/`_apply` functions with a
  `SceneAccessor` subclass that exposes properly typed `create()` and
  `apply()` methods.
- Replace timer domain's dynamic `_create` function with a
  `TimerAccessor` subclass that exposes a properly typed `create()`
  method.
- Remove all `# type: ignore[assignment]` comments from domain code;
  mypy now passes cleanly with no suppressions.
- Add six new tests covering the `factory` property, typed accessor
  instantiation, and the `accessor_cls` dispatch path.
@Faerkeren Faerkeren merged commit 942044a into main May 27, 2026
12 checks passed
@Faerkeren Faerkeren deleted the feature/typed-domain-accessors branch May 27, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace dynamic domain operations with typed accessors

1 participant